# ServiceLoaderDialectResolver.java

    package org.codefilarete.stalactite.sql;

    import java.sql.Connection;
    import java.sql.DatabaseMetaData;
    import java.sql.SQLException;
    import java.util.Comparator;
    import java.util.Map.Entry;
    import java.util.ServiceLoader;
    import java.util.Set;
    import java.util.TreeMap;
    import java.util.stream.Collectors;

    import org.codefilarete.stalactite.engine.DatabaseVendorSettings;
    import org.codefilarete.tool.Nullable;
    import org.codefilarete.tool.Strings;
    import org.codefilarete.tool.VisibleForTesting;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.exception.Exceptions;

    /**
     * Implementation of {@link DialectResolver} that gets its registered {@link Dialect}s through JVM Service Provider and looks for the most compatible
     * one thanks to a compatibility algorithm.
     *
     * This class will get available dialects and their compatibility as instances of {@link DialectResolverEntry}, themselves declared by JVM Service
     * Provider. Hence, it is expected that dialect implementors declare them through META-INF/services/DialectResolver.DialectResolverEntry
     * file. Then when {@link #determineDialect(Connection)} is invoked, database metadata are compared to compatibility given by entries: only entries
     * whom product name exactly matches database one are kept, then comparing version, the highest dialect among smaller than database one is selected.
     * For example, if database is "A wonderful database 3.8", and 3 dialects for "A wonderful database" are present with "3.1", "3.5" and "4.0" versions,
     * then the "3.5" will be selected.
     *
     * Why such algorithm ? because a dialect is expected to benefit from database features, hence its version should be close to the one of the
     * database that implements the feature, meaning at least equal but not lower : a "4.0" dialect may not be compatible with a "3.0" database. Therefore,
     * only smaller dialect versions are valuable, and among them, we take the closest one to benefit from best features. We also consider that databases
     * are retro-compatible so older dialects are still relevant.
     *
     * @author Guillaume Mary
     */
    public class ServiceLoaderDialectResolver implements DialectResolver {

    	@Override
    	public Dialect determineDialect(Connection conn) {
    		DatabaseSignet databaseSignet = DatabaseSignet.fromMetadata(conn);
    		ServiceLoader<DialectResolverEntry> dialects = ServiceLoader.load(DialectResolverEntry.class);
    		return determineDialect(dialects, databaseSignet);
    	}

    	Dialect determineDialect(Iterable<? extends DialectResolverEntry> dialects, DatabaseSignet databaseSignet) {
    		Nullable<DialectResolverEntry> matchingDialect = Nullable.nullable(giveMatchingEntry(dialects, databaseSignet));
    		return matchingDialect.map(DialectResolverEntry::getDialect).getOrThrow(
    				() -> new IllegalStateException(
    						"Unable to determine dialect to use for database \""
    								+ databaseSignet.getProductName()
    								+ " " + databaseSignet.getMajorVersion()
    								+ "." + databaseSignet.getMinorVersion()
    								+ "\" among " + Iterables.collectToList(dialects, o -> "{" + Strings.footPrint(o.getCompatibility(),
    																DatabaseSignet::toString) + "}")));
    	}

    	@Override
    	public DatabaseVendorSettings determineVendorSettings(Connection conn) {
    		DatabaseSignet databaseSignet = DatabaseSignet.fromMetadata(conn);
    		ServiceLoader<DialectResolverEntry> dialects = ServiceLoader.load(DialectResolverEntry.class);
    		return determineVendorSettings(dialects, databaseSignet);
    	}

    	DatabaseVendorSettings determineVendorSettings(Iterable<? extends DialectResolverEntry> dialects, DatabaseSignet databaseSignet) {
    		DialectResolverEntry matchingDialect = giveMatchingEntry(dialects, databaseSignet);
    		if (matchingDialect == null) {
    			throw new IllegalStateException(
    					"Unable to determine vendor settings to use for database \""
    							+ databaseSignet.getProductName()
    							+ " " + databaseSignet.getMajorVersion()
    							+ "." + databaseSignet.getMinorVersion()
    							+ "\" among " + Iterables.collectToList(dialects, o -> "{" + Strings.footPrint(o.getCompatibility(),
    							DatabaseSignet::toString) + "}"));
    		} else {
    			return matchingDialect.getVendorSettings();
    		}
    	}

    	@VisibleForTesting
    	@javax.annotation.Nullable
    	DialectResolverEntry giveMatchingEntry(Iterable<? extends DialectResolverEntry> dialects, DatabaseSignet databaseSignet) {
    		// only dialects that exactly matches database product name are kept
    		Set<DialectResolverEntry> databaseDialects = Iterables.stream(dialects)
    				.filter(entry -> entry.getCompatibility().getProductName().equals(databaseSignet.getProductName()))
    				.collect(Collectors.toSet());

    		if (databaseDialects.isEmpty()) {
    			// no dialect for database, caller will handle that
    			return null;
    		} else {
    			// sorting entries by compatibility versions to ease selection of the highest among the smaller than database version
    			// Note: we could have used the stream way of collecting things, but it's a bit less readable
    			TreeMap<DatabaseSignet, DialectResolverEntry> dialectPerSortedCompatibility = new TreeMap<>(DatabaseSignet.COMPARATOR);
    			databaseDialects.forEach(dialect -> dialectPerSortedCompatibility.merge(dialect.getCompatibility(), dialect, (c1, c2) -> {
    				// we use same properties as DatabaseSignet comparator ones since we use a TreeMap based on it
    				String printableSignet = Strings.footPrint(c1.getCompatibility(), DatabaseSignet::toString);
    				throw new IllegalStateException("Multiple dialects with same database compatibility found : " + printableSignet);
    			}));

    			// we select the highest dialect among the smaller than database version
    			Entry<DatabaseSignet, DialectResolverEntry> foundEntry = dialectPerSortedCompatibility.floorEntry(databaseSignet);
    			return foundEntry == null ? null : foundEntry.getValue();
    		}
    	}

    	/**
    	 * Storage for database product and version.
    	 */
    	public static class DatabaseSignet {

    		/**
    		 * Builds a {@link DatabaseSignet} from a connection to create the database signature from its metadata.
    		 * Could be a constructor but would require callers to handle {@link SQLException} which is quite boring, therefore this method handles it
    		 * by wrapping it into a {@link RuntimeException}
    		 *
    		 * @param connection the connection from which a database signature must be created
    		 * @return a new {@link DatabaseSignet}
    		 */
    		public static DatabaseSignet fromMetadata(Connection connection) {
    			try {
    				DatabaseMetaData databaseMetaData = connection.getMetaData();
    				return new DatabaseSignet(databaseMetaData.getDatabaseProductName(), databaseMetaData.getDatabaseMajorVersion(), databaseMetaData.getDatabaseMinorVersion());
    			} catch (SQLException e) {
    				throw Exceptions.asRuntimeException(e);
    			}
    		}

    		public static Comparator<DatabaseSignet> COMPARATOR = Comparator
    				.comparing(DatabaseSignet::getProductName)
    				.thenComparingInt(DatabaseSignet::getMajorVersion)
    				.thenComparingInt(DatabaseSignet::getMinorVersion);

    		private final String productName;

    		private final int majorVersion;

    		private final int minorVersion;

    		/**
    		 * Constructor with mandatory elements.
    		 * See {@link #fromMetadata(Connection)} to build one for a database.
    		 *
    		 * @param productName database product name, must be strictly equals to the one of database metadata, else detection algorithm will fail
    		 * @param majorVersion database product major version, as the one given by database metadata
    		 * @param minorVersion database product minor version, as the one given by database metadata
    		 * @see #fromMetadata(Connection)
    		 */
    		public DatabaseSignet(String productName, int majorVersion, int minorVersion) {
    			this.productName = productName;
    			this.majorVersion = majorVersion;
    			this.minorVersion = minorVersion;
    		}

    		public String getProductName() {
    			return productName;
    		}

    		public int getMajorVersion() {
    			return majorVersion;
    		}

    		public int getMinorVersion() {
    			return minorVersion;
    		}

    		/**
    		 * Implemented as "product name X.Y". To be used for debug or simple printing.
    		 *
    		 * @return "product name X.Y"
    		 */
    		@Override
    		public String toString() {
    			return productName + " " + majorVersion + "." + minorVersion;
    		}
    	}

    }